package org.hertsig.deckbox;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.filter;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.SneakyThrows;

import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.hertsig.deckbox.DeckFolder.DeckInfo;
import org.hertsig.entities.Deck;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;

import au.com.bytecode.opencsv.CSVReader;

import com.google.common.base.Charsets;
import com.google.common.base.Predicate;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.UncheckedExecutionException;

public class DeckboxApi {
	private static final String DECKBOX_URL = "https://deckbox.org/";
	private static final RedirectStrategy STRATEGY = new DefaultRedirectStrategy() {
		@Override
		public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
			if (super.isRedirected(request, response, context)) {
				return true;
			}
			int resCode = response.getStatusLine().getStatusCode();
			if (resCode == 301 || resCode == 302) {
				return true;
			}
			return false;
		}};
	private static final Predicate<Element> NON_SCRIPT = new Predicate<Element>() {
		@Override
		public boolean apply(Element input) {
			return !"script".equalsIgnoreCase(input.tagName());
		}};
	
	/**
	 * Creates an API instance and logs in with the given user and password, allowing you to only see public decks and collections, plus those private to the logged in user
	 */
	@SneakyThrows(IOException.class)
	public static DeckboxApi createAndLogin(String username, String password) {
		DeckboxApi api = new DeckboxApi();
		api.login(username, password);
		return api;
	}
	
	/**
	 * Creates an API instance without logged in user, allowing you to see only public decks and collections
	 */
	public static DeckboxApi create() {
		return new DeckboxApi();
	}
	
	private final HttpClient client;
	private final HttpContext context = new BasicHttpContext();
	private final CookieStore cookies = new BasicCookieStore();
	
	@Getter private DeckFolder rootFolder;
	
	private final Cache<String, Deck> deckCache = CacheBuilder.newBuilder().build();
	
	private String inventoryId;
	private String tradelistId;
	private String wishlistId;
	
	@Getter(lazy=true) private final Collection inventory = getCollection(inventoryId, 1);
	@Getter(lazy=true) private final Collection tradelist = getCollection(tradelistId, 0);
	@Getter(lazy=true) private final Collection wishlist = getCollection(wishlistId, 0);
	
	private DeckboxApi() {
		client = HttpClientBuilder.create().setRedirectStrategy(STRATEGY).build();
		context.setAttribute(HttpClientContext.COOKIE_STORE, cookies);
	}
	
	@SneakyThrows(IOException.class)
	public Deck getDeck(String deckId) {
		try {
			return deckCache.get(deckId, new DeckLoader(deckId));
		}
		catch (ExecutionException | UncheckedExecutionException e) {
			Throwables.propagateIfPossible(e.getCause(), IOException.class);
			throw new IllegalStateException("Can't happen", e.getCause());
		}
	}
	
	private InputStream getCsv(String setId) throws IOException {
		return executeAndGetContent(new HttpGet(DECKBOX_URL + "sets/export/" + setId + "?format=csv&s=&o=&v2=true"));
	}
	
	public void invalidateDeckCache(String deckId) {
		deckCache.invalidate(deckId);
	}
	
	public void invalidateDeckCache() {
		deckCache.invalidateAll();
	}
	
	private DeckFolder readFolder(String folderName, List<Node> children) {
		List<DeckFolder> subfolders = Lists.newArrayList();
		List<DeckInfo> decks = Lists.newArrayList();
		
		Iterator<Element> nodes = filter(filter(children, Element.class), NON_SCRIPT).iterator();
		while (nodes.hasNext()) {
			Element node = nodes.next();
			
			String id = node.attr("id");
			if (id.startsWith("folder_")) {
				String subfolderName = node.select("#folder_name_" + id.substring(7)).attr("data-title");
				DeckFolder subfolder = readFolder(subfolderName, nodes.next().childNodes().get(0).childNodes());
				subfolders.add(subfolder);
			}
			else if (id.startsWith("deck_")) {
				decks.add(new DeckInfo(id.substring(5), node.select("a").attr("data-title")));
			}
		}
		return new DeckFolder(folderName, subfolders, decks);
	}
	
	private void login(String username, String password) throws IOException {
		Document loginForm = executeAndParse(new HttpGet(DECKBOX_URL + "accounts/login_ajax"));
		Element form = loginForm.select("input[type=hidden][name=authenticity_token]").first();
		HttpPost post = new HttpPost(DECKBOX_URL + "accounts/login");
		post.setEntity(new UrlEncodedFormEntity(ImmutableList.of(
				new BasicNameValuePair("authenticity_token", form.attr("value")),
				new BasicNameValuePair("login", username),
				new BasicNameValuePair("password", password),
				new BasicNameValuePair("return_to", "/"),
				new BasicNameValuePair("remember_me", "off"))));
		
		Document html = executeAndParse(post);
		rootFolder = readFolder("Root", html.select("li.decks")
				.first()
				.nextSibling()
				.childNodes()
				.get(0)
				.childNodes());
		
		for (Element element : html.select(".submenu_entry a")) {
			String name = element.text().trim();
			if (name.equals("Inventory")) {
				inventoryId = element.attr("href").substring(6);
			}
			else if (name.equals("Tradelist")) {
				tradelistId = element.attr("href").substring(6);
			}
			else if (name.equals("Wishlist")) {
				wishlistId = element.attr("href").substring(6);
			}
		}
	}
	
	private Document executeAndParse(HttpUriRequest request) throws IOException {
		return Jsoup.parse(executeAndGetContent(request), "UTF-8", DECKBOX_URL);
	}
	
	protected InputStream executeAndGetContent(HttpUriRequest request) throws IOException {
		return client.execute(request, context).getEntity().getContent();
	}
	
	@SneakyThrows(IOException.class)
	private Collection getCollection(String collectionId, int indexOffset) {
		if (collectionId == null) {
			throw new IllegalStateException("Not logged in");
		}
		
		try (Reader  in = new InputStreamReader(getCsv(collectionId))) {
			return readCsv(in, indexOffset);
		}
	}
	
	public static Collection readInventory(Reader reader) {
		return readCsv(reader, 1);
	}
	
	@SneakyThrows(IOException.class)
	private static Collection readCsv(Reader in, int indexOffset) {
		try (CSVReader reader = new CSVReader(in)) {
			Collection collection = new Collection();
			String[] header = reader.readNext();
			checkState(header[0].equals("Count"));
			checkState(header[1 + indexOffset].equals("Name"));
			checkState(header[2 + indexOffset].equals("Foil"));
			checkState(header[3 + indexOffset].equals("Textless"));
			checkState(header[4 + indexOffset].equals("Promo"));
			checkState(header[5 + indexOffset].equals("Signed"));
			checkState(header[6 + indexOffset].equals("Edition"));
			checkState(header[7 + indexOffset].equals("Condition"));
			checkState(header[8 + indexOffset].equals("Language"));
			
			while (true) {
				String[] next = reader.readNext();
				if (next == null || next.length == 1) {
					break;
				}
				
				Collection.Card card = new Collection.Card(next[1 + indexOffset],
						toBoolean(next[2 + indexOffset]),
						toBoolean(next[3 + indexOffset]),
						toBoolean(next[4 + indexOffset]),
						toBoolean(next[5 + indexOffset]),
						next[6 + indexOffset],
						next[7 + indexOffset],
						next[8 + indexOffset]);
				
				collection.add(card, Integer.parseInt(next[0]));
			}
			return collection;
		}
	}
	
	private static boolean toBoolean(String text) {
		return !text.isEmpty(); 
	}

	@SneakyThrows(IOException.class)
	public static Deck readDeck(Reader in) {
		try (CSVReader reader = new CSVReader(in)) {
			Deck deck = new Deck();
			String[] header = reader.readNext();
			checkState(header[0].equals("Count"));
			checkState(header[1].equals("Name"));
			checkState(header[2].equals("Sideboard"));
			while (true) {
				String[] next = reader.readNext();
				if (next == null || next.length == 1) {
					break;
				}
				int total = Integer.parseInt(next[0]);
				String name = next[1];
				boolean sideboard = Integer.parseInt(next[2]) == 1;
				if (sideboard) {
					deck.addSideboard(name, total);
				}
				else {
					deck.addMain(name, total);
				}
			}
			return deck;
		}
	}
	
	@AllArgsConstructor
	private class DeckLoader implements Callable<Deck> {
		private final String deckId;
		
		@Override
		public Deck call() throws IOException {
			try (Reader in = new InputStreamReader(getCsv(deckId), Charsets.UTF_8)) {
				return readDeck(in);
			}
		}
	}
}
